﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using CLIFileRW;
using System.Reflection;

namespace ILMatcher
{
    public enum MatchNodeType
    {
        ExactMatch,
        Expression,
        KleeneStar,
        Load,
        Store,
        LoadConst,
        Call
    }

    //il match deve mettere i possibili bind in una variabile di istanza poi presa dal chiamante (magari con un check (has bind)
    public class MatchNode
    {
        private MatchNodeType nodeType;
        private bool hasPossibleMatches;
        private List<Match> possibleMatches;
        private readonly Query q;
        private int bindWith  = -1; //describes if and at which index it binds with a query argument

        public MatchNode(MatchNodeType type, ILCursor ilc, Query q) //fixme
        {
            nodeType = type;
            possibleMatches = new List<Match>();
            this.q = q;
        }

        #region Properties

        public int BindWith 
        { 
            get { return bindWith; } 
            set 
            {
                if (value >= CurrentQuery.QueryArgs.Length) throw new ArgumentException("Not so many arguments!");
                bindWith = value; 
            } 
        }

        public Query CurrentQuery { get { return q; } }

        public List<Match> PossibleMatches
        {
            get { return possibleMatches; }
        }

        public bool HasPossibleMatches
        {
            get { return possibleMatches.Count > 0; }
        }

        public MatchNodeType NodeType
        {
            get { return nodeType; }
        }
        #endregion

        #region Methods

        public virtual bool Match(ILCursor ilc)
        {
            //this is implemented by each subclass
            throw new NotImplementedException();
        }
        #endregion
    }

    public class ExpressionMatchNode : MatchNode
    {
        private Type returnType;
        //private List<CodeSection> possibleMatches; //fixme mettere private

        public ExpressionMatchNode(ILCursor ilc, Type returnType, Query q, bool isArg = true)
            : base(MatchNodeType.Expression, ilc, q)
        {
            if (ilc == null || returnType == null) throw new ArgumentNullException("parameters"); //fixme aggiungere questo controllo nel costruttore base
            this.returnType = returnType;
            //advanceCursor to avoid next callvirt (already managed!)
            ilc.Next();
        }

        #region Properties
        public Type ReturnType
        {
            get { return returnType; }
        }
        #endregion


        #region Methods

        public override bool Match(ILCursor ilc)
        {
            //save ilcursor state
            ilc.PushState();

            //save initial cursor position
            long initialPosition = ilc.Position;

            //skip unuseful instructions
            while (((ilc.Instr.op.Value == (int)OPCODES.Nop) || (ilc.Instr.op.Value == (int)OPCODES.Ret)) && ilc.Next())
                ;

            Type prevType = null;

            //prepare support Dictionary to find possible matches (gain informations on stack height and returned type before the actual instruction read
            List<KeyValuePair<long, Type>> stackSizes = new List<KeyValuePair<long, Type>>();

            List<KeyValuePair<long, ILInstruction>> instructions = new List<KeyValuePair<long, ILInstruction>>();
            KeyValuePair<long, ILInstruction>[] instrArray;

            long firstPosition = ilc.Position;

            //find all possible matches pairs
            int ss = ilc.OperandStack.Count;
            do
            {
                if (ilc.OperandStack.Count == ss)
                    stackSizes.Add(new KeyValuePair<long, Type>(ilc.Position, prevType));
                prevType = (ilc.OperandStack.Count > 0) ? ilc.OperandStack[0].Type.GetReflectionType() : null;

                //todo: mi serve per ricavare i codefragments utili per il binding, magari migliorare le performances
                instructions.Add(new KeyValuePair<long, ILInstruction>(ilc.Position,ilc.Instr.Clone())); 
            } while (ilc.Next());
            instrArray = instructions.ToArray();

            ILFragment[] currentBindings = CurrentQuery.CurrentEvaluationStep.Bindings;
            ILFragment[] updatedBindings = currentBindings.Clone() as ILFragment[];

            //add empty match
            if (BindWith >= 0)
                if (currentBindings[BindWith] == null) //addBinding if is the first time found
                {
                    updatedBindings = currentBindings.Clone() as ILFragment[];
                    updatedBindings[BindWith] = new ILFragment(new ILInstruction[0], initialPosition);
                }
                else
                { //fixme todo controllare che il binding attuale sia corrispondente a quello di prima altrimenti non aggiungo il match

                }
            PossibleMatches.Add(new Match(initialPosition, initialPosition, updatedBindings));            

            //add other possible matches
            for (int i = 0; i < stackSizes.Count; i++)
                if (stackSizes[i].Value != null && stackSizes[i].Value.Equals(ReturnType)) //purge incompatible return type from possible pairs
                {
                    long finalPosition = stackSizes[i].Key;
                    updatedBindings = currentBindings.Clone() as ILFragment[];

                    if (BindWith >= 0)
                    {  //todo needs refactoring!
                        List<ILInstruction> instructionSubset = new List<ILInstruction>();
                        

                        #region BuildingInstructionSubsetForBinding
                        int j = 0;
                        while (instrArray[j].Key != finalPosition) //todo: needed optimizations!
                        {
                            instructionSubset.Add(instrArray[j++].Value.Clone());
                        }
                        //instructionSubset.Add(instrArray[j].Value.Clone());
                        #endregion

                        if (currentBindings[BindWith] == null) //add Bind
                        {
                            updatedBindings[BindWith] = new ILFragment(instructionSubset.ToArray(), firstPosition, finalPosition);
                        }
                        else
                        { //todo: check di integrità, sicuramente serve un continue! comunque non deve essere preso come possible match

                        }
                    }
                    PossibleMatches.Add(new Match(initialPosition, finalPosition, updatedBindings));   
                }

            //restore ilc state
            ilc.PopState();

            //todo: ripulire un po' gli oggetti creati o ci pensa lui stesso all'uscita del metodo?
            return PossibleMatches.Count > 0;
        }

        public override string ToString()
        {
            return "" + NodeType + ": " + ReturnType.Name;
        }
        #endregion
    }

    public class ExactMatchNode : MatchNode
    {
        ILInstruction instr;

        public ExactMatchNode(ILCursor ilc, Query q) :
            base(MatchNodeType.ExactMatch, ilc, q)
        {
            instr = ilc.Instr;
        }

        #region Methods
        public override bool Match(ILCursor ilc)
        {
            bool isMatch = ilc.Instr.op.Name.Equals(instr.op.Name);
            if (isMatch)
            {
                ilc.PushState();
                long x = ilc.Position;
                ilc.Next();
                long y = ilc.Position;
                ilc.PopState();
                PossibleMatches.Add(new Match(x, y, CurrentQuery.CurrentEvaluationStep.Bindings.Clone() as ILFragment[]));
            }
            return isMatch;
        }

        public override string ToString()
        {
            return "" + NodeType + ": " + instr.op.Name;
        }

        #endregion
    }

    public class LoadConstMatchNode : MatchNode //fixme: aggiungere anche il tipo di store? stloc, starg, starga,...?
    {
        private object parameter;
        private Type constType;

        public LoadConstMatchNode(ILCursor ilc, Type constType, Query q)
            : base(MatchNodeType.LoadConst, ilc, q)
        {
            parameter = ILInstruction.Normalize(ilc.Instr).ResolveParameter(ilc.FileReader);
            this.constType = constType;
        }

        #region Properities
        public object Parameter
        {
            get { return parameter; }
        }
        #endregion

        #region Methods
        private Type getInstrParamType(ILInstruction i)
        {
            ILInstruction instr = ILInstruction.Normalize(i);
            OPCODES op = (OPCODES)((instr.op.Value < 0 ? 256 : 0) + (instr.op.Value & 0xFF));
            switch (op)
            {
                case OPCODES.Ldc_I4:
                case OPCODES.Ldc_I4_S:
                    return typeof(Int32);
                case OPCODES.Ldc_I8:
                    return typeof(Int64);
                case OPCODES.Ldc_R4:
                    return typeof(Single);
                case OPCODES.Ldc_R8:
                    return typeof(Double);
                default:
                    throw new ArgumentException();
            }
        }

        public override bool Match(ILCursor ilc)
        {
            //bool isMatch = isLdc(ilc.Instr) && ILInstruction.Normalize(ilc.Instr).ResolveParameter(ilc.FileReader).Equals(parameter); 
            //fixme aggiungere il binding??? forse no in questo caso;

            bool isMatch = isLdc(ilc.Instr) && constType.Equals(getInstrParamType(ilc.Instr)); //fixme: al posto di null ci va 
            if (isMatch)
            {
                ilc.PushState();
                long x = ilc.Position;
                ilc.Next();
                long y = ilc.Position;
                ilc.PopState();
                PossibleMatches.Add(new Match(x, y, CurrentQuery.CurrentEvaluationStep.Bindings.Clone() as ILFragment[]));
            }
            return isMatch;
        }

        private bool isLdc(ILInstruction instr)
        {
            return instr.op.Value == (int)OPCODES.Ldc_I4_2; //fixme aggiungere le altre lodc oppure fare normalize
        }

        public override string ToString()
        {
            return "" + NodeType + ": ldc " + parameter.ToString();
        }
        #endregion
    }

    public class StoreMatchNode : MatchNode  //fixme fare con il parametro non con la posizione! tipo la load!
    {
        int storePosition;

        public StoreMatchNode(ILCursor ilc, int position, Query q)
            : base(MatchNodeType.Store, ilc, q)
        {
            storePosition = position;
        }

        #region Properties
        public int StorePosition
        {
            get { return storePosition; }
        }
        #endregion

        #region Methods

        public override bool Match(ILCursor ilc)
        {
            bool isMatch = isStore(ilc.Instr); //fixme: aggiungere il binding
            if (isMatch)
            {
                ilc.PushState();
                long x = ilc.Position;
                ilc.Next();
                long y = ilc.Position;
                PossibleMatches.Add(new Match(x, y, CurrentQuery.CurrentEvaluationStep.Bindings.Clone() as ILFragment[]));
                ilc.PopState();
            }

            return isMatch;
        }

        public override string ToString()
        {
            return "" + NodeType + ": " + 1;
        }

        private bool isStore(ILInstruction instr)
        {
            return (instr.op.Value == (int)OPCODES.Stloc_0) || (instr.op.Value == (int)OPCODES.Stloc_1) || (instr.op.Value == (int)OPCODES.Stloc_2)
                || (instr.op.Value == (int)OPCODES.Stloc_3) || (instr.op.Value == (int)OPCODES.Stloc_S); //fixme aggiungere le altre store? stobj ?
        }
        #endregion
    }

    public class CallMatchNode : MatchNode
    {
        private MethodBase methodCalled;
        private OPCODES opcode;

        public CallMatchNode(ILCursor ilc, Query q)
            : base(MatchNodeType.Call, ilc, q)
        {

            methodCalled = ((MethodDesc)ilc.Instr.ResolveParameter(ilc.FileReader)).GetReflectionMethod(null);
            opcode = Query.getOpcode(ilc.Instr);
        }

        public override bool Match(ILCursor ilc)
        {
            bool isMatch = opcode == Query.getOpcode(ilc.Instr) && methodCalled.Equals(((MethodDesc)ilc.Instr.ResolveParameter(ilc.FileReader)).GetReflectionMethod(null));
            if (isMatch)
            {
                ilc.PushState();
                long x = ilc.Position;
                ilc.Next();
                long y = ilc.Position;
                PossibleMatches.Add(new Match(x, y, CurrentQuery.CurrentEvaluationStep.Bindings.Clone() as ILFragment[]));
                ilc.PopState();
            }
            return isMatch;
        }

        public override string ToString()
        {
            return "" + opcode + "; " + methodCalled.Name;
        }
    }

    public class KleeneMatchNode : MatchNode
    {
        public KleeneMatchNode(ILCursor ilc, Query q)
            : base(MatchNodeType.KleeneStar, ilc, q)
        {
            ilc.Next(); //to avoid reading next callvirt to delegate invocation
        }

        #region Methods

        public override bool Match(ILCursor ilc)
        {

            //fixme controllare che sia sempre giusto
            if (ilc.OperandStack.Count != 0) return false;

            ilc.PushState();

            long initialPosition = ilc.Position;
            long finalPosition = initialPosition;

            //fixme: controlla quale delle due è giusta
            //while (ilc.Next()){
            //    if (ilc.OperandStack.Count == 0 && Query.getOpcode(ilc.Instr) != OPCODES.Nop && Query.getOpcode(ilc.Instr) != OPCODES.Ret) //controlla condizione e ottimizza la doppia chiamata
            //        finalPosition = ilc.Position;
            //PossibleMatches.Add(new CodeSection(0, initialPosition, finalPosition));

            ILFragment[] currentBindings = CurrentQuery.CurrentEvaluationStep.Bindings;
            ILFragment[] updatedBindings = currentBindings.Clone() as ILFragment[];

            //add empty match
            if (BindWith >= 0)
                if (currentBindings[BindWith] == null) //addBinding if is the first time found
                {
                    updatedBindings[BindWith] = new ILFragment(new ILInstruction[0], initialPosition);
                }
                else
                { //fixme todo controllare che il binding attuale sia corrispondente a quello di prima altrimenti non aggiungo il match
                    
                }
            PossibleMatches.Add(new Match(initialPosition, initialPosition, updatedBindings));

            List<ILInstruction> instruction = new List<ILInstruction>();
            instruction.Add(ilc.Instr.Clone());


            while (ilc.Next())
            {
                if (ilc.OperandStack.Count == 0)
                {
                    updatedBindings = currentBindings.Clone() as ILFragment[];
                    if(BindWith >= 0)
                        if (currentBindings[BindWith] == null) //addBinding if is the first time found
                        {
                            updatedBindings[BindWith] = new ILFragment(instruction.ToArray(), initialPosition, ilc.Position);
                        }
                        else
                        { //fixme todo controllare che il binding attuale sia corrispondente a quello di prima altrimenti non aggiungo il match

                        }
                    PossibleMatches.Add(new Match(initialPosition, ilc.Position, updatedBindings));
                }
                instruction.Add(ilc.Instr.Clone());
            }
            ilc.PopState();

            return PossibleMatches.Count > 0;
        }
        #endregion
    }

    public class LoadMatchNode : MatchNode
    {
        OPCODES opcode;
        object pos;
        Type typeLoaded;

        public LoadMatchNode(ILCursor ilc, Type typeLoaded, Query q)
            : base(MatchNodeType.Load, ilc, q)
        {
            opcode = Query.getOpcode(ilc.Instr);
            pos = (ILInstruction.Normalize(ilc.Instr).ResolveParameter(ilc.FileReader));
            this.typeLoaded = typeLoaded;
        }

        #region Properties
        public OPCODES Opcode
        {
            get { return opcode; }
        }
        #endregion

        #region Method
        private bool isLocalVar(ILInstruction i)
        {
            ILInstruction instr = ILInstruction.Normalize(i);
            OPCODES op = (OPCODES)((instr.op.Value < 0 ? 256 : 0) + (instr.op.Value & 0xFF));
            switch (op)
            {
                case OPCODES.Ldarg:
                    return false;
                case OPCODES.Ldloc:
                    return true;
                default:
                    throw new ArgumentException();
            }
        }

        public override bool Match(ILCursor ilc)
        {
            if (isLoad(ilc.Instr))
            {
                object param = ILInstruction.Normalize(ilc.Instr).ResolveParameter(ilc.FileReader);

                if (param == null)
                    return false; //fixme: non dovrebbe essere possibile


                bool isMatch;
                if (Query.getOpcode(ilc.Instr) != OPCODES.Ldstr)
                {
                    int pos = int.Parse(param.ToString());
                    isMatch = typeLoaded.Equals((isLocalVar(ilc.Instr) ? CurrentQuery.Vars[pos] : CurrentQuery.Args[pos]).GetReflectionType()); //type equivalence, isLoad already done

                }
                else
                    isMatch = typeLoaded.Equals(typeof(string));

                if (isMatch)
                {
                    ILFragment[] currentBindings = CurrentQuery.CurrentEvaluationStep.Bindings;
                    ILFragment[] updatedBindings = currentBindings.Clone() as ILFragment[];
                    if (BindWith >= 0){
                        ILInstruction[] bindInstr = new ILInstruction[1];
                        bindInstr[0] = ilc.Instr.Clone();

                        if (currentBindings[BindWith] == null)
                        {
                            updatedBindings[BindWith] = new ILFragment(bindInstr, ilc.Position);
                        }
                        else 
                        { //todo: check di integrità
                        
                        }
                    }
                    ilc.PushState();
                    long x = ilc.Position;
                    ilc.Next();
                    long y = ilc.Position;
                    PossibleMatches.Add(new Match(x, y, updatedBindings));
                    ilc.PopState();
                }
                return isMatch;
            }
            return false;
            //todo aggiungere le operazioni per la binding table
        }


        private bool isLoad(ILInstruction instr)
        {
            switch (Query.getOpcode(instr))
            {
                case OPCODES.Ldarg:
                case OPCODES.Ldarg_0:
                case OPCODES.Ldarg_1:
                case OPCODES.Ldarg_2:
                case OPCODES.Ldarg_3:
                case OPCODES.Ldarg_S:
                //case OPCODES.Ldarga: fixme: controllare che queste ci vogliano e che la semantica sia uguale
                //case OPCODES.Ldarga_S:
                //case OPCODES.Ldelem e questi? verificare
                case OPCODES.Ldloc:
                case OPCODES.Ldloc_0:
                case OPCODES.Ldloc_1:
                case OPCODES.Ldloc_2:
                case OPCODES.Ldloc_3:
                case OPCODES.Ldloc_S:
                case OPCODES.Ldstr:
                    return true;
                //case OPCODES.Ldloca: constollare
                //case OPCODES.Ldloca_S
                default:
                    return false;

            }
        }
        public override string ToString()
        {
            return "" + opcode + " pos: " + pos.ToString() + ", " + typeLoaded.Name;
        }
        #endregion
    }


}
